from threading import Thread, Lock
from time import sleep
from tkinter import filedialog, Tk
from pandas import read_excel
from pynput import keyboard
from win32api import SetCursorPos, mouse_event
from win32clipboard import OpenClipboard, EmptyClipboard, SetClipboardData, CloseClipboard
from win32gui import FindWindow, GetWindowPlacement, ShowWindow, SetWindowPos, PostMessage
from win32con import MOUSEEVENTF_LEFTDOWN, MOUSEEVENTF_LEFTUP, CF_UNICODETEXT, SW_SHOWDEFAULT, HWND_TOPMOST, \
    HWND_NOTOPMOST, SWP_SHOWWINDOW, SW_SHOWNORMAL, WM_CLOSE
from pykeyboard import PyKeyboard
# pykeyboard==0.1.2 需要先安装PyUserInput，pyHook, PyWin32(这个应该有自带)
from pyperclip import copy
from argparse import ArgumentParser
from datetime import datetime
from sys import setrecursionlimit, exit
from os import system

setrecursionlimit(100000)  # 设置递归深度

__author__ = '广大菜鸟'
isEnd = False
listener = None


def on_activate():
    print('中断热键 <shift> 已启动.在此为本次程序异常对您造成的不便感到十分抱歉！！！')
    global isEnd
    isEnd = True
    return False


def for_canonical(f):
    global listener
    return lambda k: f(listener.canonical(k))


hotkey = keyboard.HotKey(
    keyboard.HotKey.parse('<shift>'),
    on_activate)


def closeWrongWindow() -> bool:
    sleep(0.2)
    windowHandle = FindWindow("MsgManagerWindow", u"消息管理器")  # 可以通过winSpy获得
    max_iter = 4
    while windowHandle == 0 and max_iter > 0:  # 设置1秒内5次检查是否有消息管理器这个错误窗口
        sleep(0.2)
        windowHandle = FindWindow("MsgManagerWindow", u"消息管理器")
        max_iter -= 1
    if windowHandle != 0:
        PostMessage(windowHandle, WM_CLOSE, 0, 0)
        return True  # 表示目前是关闭了错误弹窗
    return False


def getWorkingLocation():
    window = FindWindow("WeWorkWindow", u"企业微信")  # 可以通过winSpy获得
    max_iter = 100
    while window == 0 and max_iter > 0:
        sleep(0.2)
        print("在定位窗口句柄时，发现为无效的窗口句柄。请检查是否打开企业微信的窗口后再重试此软件")
        window = FindWindow("WeWorkWindow", u"企业微信")  # 可以通过winSpy获得
        max_iter -= 1
    ShowWindow(window, SW_SHOWDEFAULT)  # 设置大小为正常模式,处理开启但没显示，最小化和最大化的情况
    return window, GetWindowPlacement(window)


# 输入学生的学号，前提：需要打开电脑的企业微信
def openUser(info, lang=None, time=None):
    k = PyKeyboard()
    # 0、 选择搜索页面
    window, loginid = getWorkingLocation()
    [left, top, _, _] = loginid[4]
    # [left, top, right, bottom] = loginid[4]
    if left < 0:  # 处理左侧被挡住
        left = 10
    width, height = 739, 635  # 防止有的人拉太大
    SetWindowPos(window, HWND_TOPMOST, left, top, width, height, SWP_SHOWWINDOW)  # 先设置为置顶,不可以改变位置
    SetWindowPos(window, HWND_NOTOPMOST, left, top, width, height,
                 SWP_SHOWWINDOW)  # 修改为可改变在前面的软件界面，考虑到在遇到意外需要手动关闭终端
    # 通过像素获取左侧聊天界面按钮，同时暂时覆盖其他软件
    foreX, foreY = int(left + 31 / 897 * width), int(top + height * 110 / 712)  # 比值是通过图片像素比值获得聊天首页面
    SetCursorPos((foreX, foreY))
    mouse_event(MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0)  # press mouse
    mouse_event(MOUSEEVENTF_LEFTUP, 0, 0, 0, 0)  # release mouse

    waitForReactionTime = 1 if time is None or time < 1 else time
    firstX, firstY = int(left + 154 / 919 * width), int(top + height * 48 / 650)  # 比值是通过图片像素比值获得搜索框
    secondX, secondY = int(left + 154 / 919 * width), int(top + height * 108 / 650)  # 比值是通过图片像素比值获得名片选择框
    thirdX, thirdY = int(left + 555 / 858 * width), int(top + height * 591 / 650)  # 比值是通过图片像素比值获得对话框

    global isEnd
    global listener

    listener = keyboard.Listener(
        on_press=for_canonical(hotkey.press))
    listener.daemon = 1

    # 创建、启动鼠标监听线程
    t2 = Thread(target=sendInfo,
                args=(info, k, firstX, firstY, secondX, secondY, thirdX, thirdY, waitForReactionTime, lang))
    t2.daemon = 1
    listener.start()
    t2.start()

    while True:
        if isEnd:
            return


lock = Lock()  # 申请一把锁


def sendInfo(info, k, firstX, firstY, secondX, secondY, thirdX, thirdY, waitForReactionTime, lang):
    global isEnd

    for (sno, sname) in info:

        if isEnd:
            return
        k.press_key(k.control_key)  # ctrl+f
        k.tap_key('f')
        k.release_key(k.control_key)
        # 1、在输入框输入用户学号  ：发现快捷键ctrl-f可以定位到搜索框，但为了可靠，还是手动点下位置
        SetCursorPos((firstX, firstY))
        mouse_event(MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0)  # press mouse
        mouse_event(MOUSEEVENTF_LEFTUP, 0, 0, 0, 0)  # release mouse

        lock.acquire()  # 加锁

        copy(sno)  # 复制到剪贴板
        k.press_key(k.control_key)  # ctrl+a
        k.tap_key('a')
        k.release_key(k.control_key)
        k.press_key(k.control_key)  # ctrl+v
        k.tap_key('v')
        k.release_key(k.control_key)
        k.press_key(k.enter_key)  # 发送
        k.release_key(k.enter_key)

        lock.release()  # 释放锁

        sleep(waitForReactionTime)  # 给等待响应时间

        # 2、选中用户下拉框第一个元素
        SetCursorPos((secondX, secondY))
        mouse_event(MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0)  # press mouse
        mouse_event(MOUSEEVENTF_LEFTUP, 0, 0, 0, 0)  # release mouse

        if closeWrongWindow():  # 表示目前光标进入了错误的对话框
            print('没有通知到位的学生：' + sno + " " + sname)
            continue

        # 3、输入信息
        msg = "你好，" + sname + "同学。"
        if lang is not None:
            msg = msg + lang
        else:
            msg += "温馨提醒，疫情防控，从我做起。截止于下午3点半，系统显示您今日还没有上报健康信息，请尽快上报。"

        SetCursorPos((thirdX, thirdY))
        mouse_event(MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0)  # press mouse
        mouse_event(MOUSEEVENTF_LEFTUP, 0, 0, 0, 0)  # release mouse

        # 打开剪贴板
        OpenClipboard()
        # 清空剪贴板
        EmptyClipboard()

        # 设置剪贴板内容
        lock.acquire()  # 加锁
        SetClipboardData(CF_UNICODETEXT, msg)
        CloseClipboard()

        k.press_key(k.control_key)  # ctrl+v
        k.tap_key('v')
        k.release_key(k.control_key)
        k.press_key(k.enter_key)  # 发送
        k.release_key(k.enter_key)

        lock.release()  # 释放锁

        sleep(0.1)  # 给检查和处理异常的有效时间,线程可以切换检查是否有按下快捷键

    isEnd = True


def readFromExcelByPandas(filename):
    try:
        df = read_excel(filename, header=1, index_col=0, dtype=str)
        return df
    except Exception as e:
        print(str(e))
        return None


def informUsers(src, classno=None, lang=None, waitTime=None, delGrade=None):
    itemDataFrame = readFromExcelByPandas(src)
    if itemDataFrame is None:
        return
    itemDataFrame.fillna(' ', inplace=True)

    if classno is not None and '班级' in itemDataFrame.columns:
        target = itemDataFrame.loc[itemDataFrame['班级'] == classno]  # 建立在表格的用户的sno都存在
    else:
        target = itemDataFrame

    # 默认删除17级
    if '班级' in target.columns:
        target = target.loc[~ target['班级'].str.contains('[^0-9]+' + str(17) + '\d')]  # 默认先删除17级
        if delGrade is not None and 10 < delGrade < 100:  # 目前可见年级有17-20，所以合理范围为2位数 去除
            target = target.loc[~ target['班级'].str.contains('[^0-9]+' + str(delGrade) + '\d')]  # 正则表达式匹配 如"计科214"的21

    print("本次需要选择的对象数目有：", len(target))
    # openUser(list(target.groupby(['学号', '姓名']).groups.keys()), lang) # 按照学号排序
    if len(target) > 0:
        openUser(target[['学号', '姓名']].values.tolist(), lang, waitTime)  # 按原来的顺序


def informUser(sno, sname, lang=None, waitTime=None):
    openUser([(sno, sname)], lang, waitTime)


# 选择文件
def selectFilename():
    root = Tk()
    root.withdraw()
    filename = filedialog.askopenfilename()
    if filename != '':
        print("您选择的文件是：" + filename)
        return filename
    else:
        raise Exception("您没有选择任何文件")


def main():
    parser = ArgumentParser()
    parser.description = '本软件功能是企业微信打卡，可以选择文件/个人模式(注：确定企业微信最新版，在启动企业微信和该程序后，不要移动鼠标和点击键盘)\n' \
                         '\n默认发送消息格式为\'你好，xxx同学。\'\n' \
                         '简单使用案例：企业微信打卡提醒 -c *  -r 21 (意思是删除表中21届的学生，不给于通知)'
    parser.add_argument("-c", "--classno", help="这个参数是班级名称(当*时，为操作所有班级数据)", type=str)
    parser.add_argument("-o", "--sno", help="这个参数是学生学号", type=str)
    parser.add_argument("-a", "--sname", help="这个参数是学生名字", type=str)
    parser.add_argument("-l", "--lang", help="这个参数是在基本发送格式[你好，xxx同学。]增加的文字。如不设置，则输出自定的格式。", type=str)
    parser.add_argument("-r", "--delGrade", help="这个参数是删除年级（2位数字，如17）", type=int, default=17)
    parser.add_argument("-t", "--time", help="这个参数是等待响应时间（可以根据网速增加,默认1(单位：秒)，只能多不能少，少了肯定有异常)", type=float)
    args = parser.parse_args()

    if args.sname is not None and args.sno is not None:
        oldtime = datetime.now()
        try:
            informUser(args.sno, args.sname, args.lang, args.time)
        except Exception as e:
            print(str(e))
        finally:
            newtime = datetime.now()
            print("本次通知使用时间为%.2fs" % ((newtime - oldtime).total_seconds()))
    elif args.classno is not None:  # 文件+班级模式
        classno = args.classno if args.classno != '*' else None
        oldtime = datetime.now()  # 包括选择文件的时间
        try:
            srcFilePath = selectFilename()
            informUsers(srcFilePath, classno, args.lang, args.time, args.delGrade)
        except Exception as e:
            print(str(e))
        finally:
            newtime = datetime.now()
            print("本次通知使用时间为%.2fs" % ((newtime - oldtime).total_seconds()))
    else:
        try:
            parser.error('\n请使用终端命令行启动本软件，并输入参数\n选择文件模式时，需要指定班级名称或者用*号代表所有班级;\n选择学生模式时，需要指定学生学号和学生名字')
        except Exception as e:
            print(str(e))
        finally:
            system("pause")


if __name__ == '__main__':
    main()
 